const Deferred = (function (global) {
  function isArray(arr) {
    return Object.prototype.toString.call(arr) === '[object Array]';
  }

  function foreach(arr, handler) {
    if (isArray(arr)) {
      for (var i = 0; i < arr.length; i++) {
        handler(arr[i]);
      }
    }
    else
      handler(arr);
  }

  function D(fn) {
    var status = 'pending',
      doneFuncs = [],
      failFuncs = [],
      progressFuncs = [],
      resultArgs = null,

      promise = {
        done: function () {
          for (var i = 0; i < arguments.length; i++) {
            // skip any undefined or null arguments
            if (!arguments[i]) {
              continue;
            }

            if (isArray(arguments[i])) {
              var arr = arguments[i];
              for (var j = 0; j < arr.length; j++) {
                // immediately call the function if the deferred has been resolved
                if (status === 'resolved') {
                  arr[j].apply(this, resultArgs);
                }

                doneFuncs.push(arr[j]);
              }
            }
            else {
              // immediately call the function if the deferred has been resolved
              if (status === 'resolved') {
                arguments[i].apply(this, resultArgs);
              }

              doneFuncs.push(arguments[i]);
            }
          }

          return this;
        },

        fail: function () {
          for (var i = 0; i < arguments.length; i++) {
            // skip any undefined or null arguments
            if (!arguments[i]) {
              continue;
            }

            if (isArray(arguments[i])) {
              var arr = arguments[i];
              for (var j = 0; j < arr.length; j++) {
                // immediately call the function if the deferred has been resolved
                if (status === 'rejected') {
                  arr[j].apply(this, resultArgs);
                }

                failFuncs.push(arr[j]);
              }
            }
            else {
              // immediately call the function if the deferred has been resolved
              if (status === 'rejected') {
                arguments[i].apply(this, resultArgs);
              }

              failFuncs.push(arguments[i]);
            }
          }

          return this;
        },

        always: function () {
          return this.done.apply(this, arguments).fail.apply(this, arguments);
        },

        progress: function () {
          for (var i = 0; i < arguments.length; i++) {
            // skip any undefined or null arguments
            if (!arguments[i]) {
              continue;
            }

            if (isArray(arguments[i])) {
              var arr = arguments[i];
              for (var j = 0; j < arr.length; j++) {
                // immediately call the function if the deferred has been resolved
                if (status === 'pending') {
                  progressFuncs.push(arr[j]);
                }
              }
            }
            else {
              // immediately call the function if the deferred has been resolved
              if (status === 'pending') {
                progressFuncs.push(arguments[i]);
              }
            }
          }

          return this;
        },

        then: function () {
          // fail callbacks
          if (arguments.length > 1 && arguments[1]) {
            this.fail(arguments[1]);
          }

          // done callbacks
          if (arguments.length > 0 && arguments[0]) {
            this.done(arguments[0]);
          }

          // notify callbacks
          if (arguments.length > 2 && arguments[2]) {
            this.progress(arguments[2]);
          }
        },

        promise: function (obj) {
          if (obj == null) {
            return promise;
          } else {
            for (var i in promise) {
              obj[i] = promise[i];
            }
            return obj;
          }
        },

        state: function () {
          return status;
        },

        debug: function () {
          console.log('[debug]', doneFuncs, failFuncs, status);
        },

        isRejected: function () {
          return status === 'rejected';
        },

        isResolved: function () {
          return status === 'resolved';
        },

        pipe: function (done, fail, progress) {
          return D(function (def) {
            foreach(done, function (func) {
              // filter function
              if (typeof func === 'function') {
                deferred.done(function () {
                  var returnval = func.apply(this, arguments);
                  // if a new deferred/promise is returned, its state is passed to the current deferred/promise
                  if (returnval && typeof returnval === 'function') {
                    returnval.promise().then(def.resolve, def.reject, def.notify);
                  }
                  else {	// if new return val is passed, it is passed to the piped done
                    def.resolve(returnval);
                  }
                });
              }
              else {
                deferred.done(def.resolve);
              }
            });

            foreach(fail, function (func) {
              if (typeof func === 'function') {
                deferred.fail(function () {
                  var returnval = func.apply(this, arguments);

                  if (returnval && typeof returnval === 'function') {
                    returnval.promise().then(def.resolve, def.reject, def.notify);
                  } else {
                    def.reject(returnval);
                  }
                });
              }
              else {
                deferred.fail(def.reject);
              }
            });
          }).promise();
        }
      },

      deferred = {
        resolveWith: function (context) {
          if (status === 'pending') {
            status = 'resolved';
            var args = resultArgs = (arguments.length > 1) ? arguments[1] : [];
            for (var i = 0; i < doneFuncs.length; i++) {
              doneFuncs[i].apply(context, args);
            }
          }
          return this;
        },

        rejectWith: function (context) {
          if (status === 'pending') {
            status = 'rejected';
            var args = resultArgs = (arguments.length > 1) ? arguments[1] : [];
            for (var i = 0; i < failFuncs.length; i++) {
              failFuncs[i].apply(context, args);
            }
          }
          return this;
        },

        notifyWith: function (context) {
          if (status === 'pending') {
            var args = resultArgs = (arguments.length > 1) ? arguments[1] : [];
            for (var i = 0; i < progressFuncs.length; i++) {
              progressFuncs[i].apply(context, args);
            }
          }
          return this;
        },

        resolve: function () {
          return this.resolveWith(this, arguments);
        },

        reject: function () {
          return this.rejectWith(this, arguments);
        },

        notify: function () {
          return this.notifyWith(this, arguments);
        }
      }

    var obj = promise.promise(deferred);

    if (fn) {
      fn.apply(obj, [obj]);
    }

    return obj;
  }

  D.when = function () {
    if (arguments.length < 2) {
      var obj = arguments.length ? arguments[0] : undefined;
      if (obj && (typeof obj.isResolved === 'function' && typeof obj.isRejected === 'function')) {
        return obj.promise();
      }
      else {
        return D().resolve(obj).promise();
      }
    }
    else {
      return (function (args) {
        var df = D(),
          size = args.length,
          done = 0,
          rp = new Array(size);	// resolve params: params of each resolve, we need to track down them to be able to pass them in the correct order if the master needs to be resolved

        for (var i = 0; i < args.length; i++) {
          (function (j) {
            var obj = null;

            if (args[j].done) {
              args[j].done(function () { rp[j] = (arguments.length < 2) ? arguments[0] : arguments; if (++done == size) { df.resolve.apply(df, rp); } })
                .fail(function () { df.reject(arguments); });
            } else {
              obj = args[j];
              args[j] = new Deferred();

              args[j].done(function () { rp[j] = (arguments.length < 2) ? arguments[0] : arguments; if (++done == size) { df.resolve.apply(df, rp); } })
                .fail(function () { df.reject(arguments); }).resolve(obj);
            }
          })(i);
        }

        return df.promise();
      })(arguments);
    }
  }

  //global.Deferred = D;
  return D;
})(window);

module.exports = {
  Deferred: Deferred
}